/**
 *    Copyright 2009-2021 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.builder.xml;

import org.apache.ibatis.builder.BaseBuilder;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;

import java.util.List;
import java.util.Locale;

/**
 * Xml语句构建，主要是select,insert,update,delete这些操作语句
 * @author Clinton Begin
 */
public class XMLStatementBuilder extends BaseBuilder {
  /**
   * mapper构建租售
   */
  private final MapperBuilderAssistant builderAssistant;
  /**
   * 操作节点信息
   */
  private final XNode context;
  /**
   * 必要的databaseId,可能为null，这是当前系统的数据库ID
   * 可能存在不配置的情况
   */
  private final String requiredDatabaseId;

  public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context) {
    this(configuration, builderAssistant, context, null);
  }

  public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context, String databaseId) {
    super(configuration);
    this.builderAssistant = builderAssistant;
    this.context = context;
    this.requiredDatabaseId = databaseId;
  }

  /**
   * 解析执行语句(select,insert,update,delete)节点，如下例子
   * <select id="getAllProduct" resultType="product" databaseId="mysql">
   * 	SELECT * FROM product
   * </select>
   */
  public void parseStatementNode() {
    // 获取id 和 databaseId属性
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    // 验证数据库id是否符合当前的
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    // 获取节点名称
    String nodeName = context.getNode().getNodeName();
    // 转换操作的类型
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    // 是否是查询
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    // 是否清除缓存，如果是select默认不更新缓存,如果不是，更新缓存
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    // 是否使用缓存，如果是select默认使用缓存，如果不是，不更新缓存
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    // 结果排序，默认false
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    // 处理如下的include标签信息
    // <select id="selectById" resultMap="BaseResultMap">
    //        select
    //            my.*
    //        FROM
    //            sys_user my
    //        <include refid="test">
    //            <property name="testVal" value="1"/>
    //        </include>
    //    </select>
    //
    //    <sql id="test">
    //        <if test="${testVal} != null">
    //            WHERE my.id = ${testVal}
    //        </if>
    //    </sql>
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // 获取parameterType，并且转换类型
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    // 获取lang属性，根据属性获取驱动
    String lang = context.getStringAttribute("lang");
    // 默认返回的XMLLanguageDriver
    LanguageDriver langDriver = getLanguageDriver(lang);

    // 处理selectKey标签的节点
    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    // 在这里已经完成selectKey 的解析以及include标签的移除
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    // 应用当前的命名空间，比如 com.zxm.test.mapper.userMaper.insertUser!selectKey
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);

    if (configuration.hasKeyGenerator(keyStatementId)) {
      // 是否已经拥有这个key
      // 如果拥有,从中直接获取，比如存在selectKey标签的时候，默认会使用SelectKeyGenerator
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      // 获取useGeneratedKeys属性，如果存在则直接使用，如果不存在
      // 检查是否设置了isUseGeneratedKeys，如果设置了，而且是insert操作，使用Jdbc3KeyGenerator，否则使用NoKeyGenerator
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }
    // 获取数据源，如果动态语句返回的是DynamicSqlSource如果是静态的(select * from user）返回的静态数据源RawSqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    // 获取各种属性
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");
    // 添加到执行语句sql集合中，这个集合在configuration中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

  /**
   * 处理selectKey标签的节点
   * 新增数据，得到主键的外层写法没什么特别，跟普通的insert一样。只不过里面加了selectKey
   * selectKey 会将 SELECT LAST_INSERT_ID()的结果放入到传入的model的主键里面，其中属性：
   * 1）keyProperty 对应的model中的主键的属性名，这里是 user 中的id，因为它跟数据库的主键对应
   * 2）order
   * 	AFTER 表示 SELECT LAST_INSERT_ID() 在insert执行之后执行,多用与自增主键，
   * 	BEFORE 表示 SELECT LAST_INSERT_ID() 在insert执行之前执行，这样的话就拿不到主键了，这种适合那种主键不是自增的类型
   * 3）resultType 主键类型
   *     <insert id="insertAndgetkey" parameterType="com.soft.mybatis.model.User">
   *         <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Integer">
   *             SELECT LAST_INSERT_ID()
   *         </selectKey>
   *         insert into t_user (username,password,create_date) values(#{username},#{password},#{createDate})
   *     </insert>
   * @param id id
   * @param parameterTypeClass 参数类型
   * @param langDriver 语言驱动
   */
  private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
    List<XNode> selectKeyNodes = context.evalNodes("selectKey");
    if (configuration.getDatabaseId() != null) {
      parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
    }
    parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
    removeSelectKeyNodes(selectKeyNodes);
  }

  /**
   * 处理selectKey标签的节点
   * @param parentId 父节点ID，insert,update等节点的id
   * @param list 节点列表
   * @param parameterTypeClass 参数类型
   * @param langDriver 语言处理模型
   * @param skRequiredDatabaseId 数据库id,不一定会设置
   */
  private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
    // selectKey 节点列表
    for (XNode nodeToHandle : list) {
      // 设置ID
      String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
      // 指定数据库ID，处理大概如下的状况
      // <insert id="insert">
      //  <selectKey keyProperty="id" resultType="int" order="BEFORE" >
      //    <if test="_databaseId == 'oracle'">
      //      select seq_users.nextval from dual
      //    </if>
      //    <if test="_databaseId == 'db2'">
      //      select nextval for seq_users from sysibm.sysdummy1"
      //    </if>
      //  </selectKey>
      //  insert into users values (#{id}, #{name})
      //</insert>
      String databaseId = nodeToHandle.getStringAttribute("databaseId");
      if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
        parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
      }
    }
  }

  /**
   * 解析selectKey标签的节点
   * @param id ID
   * @param nodeToHandle 要处理的Node
   * @param parameterTypeClass 参数类型
   * @param langDriver 语言驱动器
   * @param databaseId 数据库id
   */
  private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
    // 获取基本的参数和属性
    String resultType = nodeToHandle.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
    String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
    boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

    // defaults
    // 默认设置，缓存，结果排序
    boolean useCache = false;
    boolean resultOrdered = false;
    // KEY生成器，但是这里这个不会做任何处理
    KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
    Integer fetchSize = null;
    Integer timeout = null;
    boolean flushCache = false;
    String parameterMap = null;
    String resultMap = null;
    ResultSetType resultSetTypeEnum = null;
    // 创建sqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
    SqlCommandType sqlCommandType = SqlCommandType.SELECT;

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

    id = builderAssistant.applyCurrentNamespace(id, false);

    MappedStatement keyStatement = configuration.getMappedStatement(id, false);
    configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
  }

  private void removeSelectKeyNodes(List<XNode> selectKeyNodes) {
    for (XNode nodeToHandle : selectKeyNodes) {
      nodeToHandle.getParent().getNode().removeChild(nodeToHandle.getNode());
    }
  }

  /**
   * databaseId是否符合当前的
   * @param id 当前操作的id
   * @param databaseId 数据库id
   * @param requiredDatabaseId 需要的requestDatabaseId
   * @return 返回是否符合当前数据库ID
   */
  private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
    if (requiredDatabaseId != null) {
      return requiredDatabaseId.equals(databaseId);
    }
    if (databaseId != null) {
      return false;
    }
    id = builderAssistant.applyCurrentNamespace(id, false);
    if (!this.configuration.hasStatement(id, false)) {
      return true;
    }
    // skip this statement if there is a previous one with a not null databaseId
    MappedStatement previous = this.configuration.getMappedStatement(id, false); // issue #2
    return previous.getDatabaseId() == null;
  }

  /**
   * 获取语言驱动
   * @param lang 语言配置
   * @return 返回语言驱动
   */
  private LanguageDriver getLanguageDriver(String lang) {
    Class<? extends LanguageDriver> langClass = null;
    if (lang != null) {
      langClass = resolveClass(lang);
    }
    return configuration.getLanguageDriver(langClass);
  }

}
